using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization;

namespace Rsdn.SmartApp.Configuration
{
	internal class ConfigSerializer : IDisposable
	{
		private readonly static XmlSerializer _configSerializer =
			new XmlSerializer(typeof(Config));
		private static readonly object _settingsCreateFlag = new object();
		private static XmlReaderSettings _readerSettings;

		private readonly IConfigDataProvider _dataProvider;
		private Dictionary<XName, List<XElement>> _loadedSections;
		private readonly object _configLoadLockFlag = new object();
		private readonly HashSet<IConfigDataProvider> _providers =
			new HashSet<IConfigDataProvider>();
		private readonly VarsContainer _rootVars;
		private Regex _varsRegex = new Regex(@"\$\(([A-Za-z][A-Za-z0-9\-_]*)\)",
			RegexOptions.Compiled);

		public ConfigSerializer(IConfigDataProvider dataProvider,
			IDictionary<string, string> rootVars)
		{
			_dataProvider = dataProvider;
			_rootVars = new VarsContainer(rootVars);
		}

		private static XmlReader GetReader(XmlReader innerReader)
		{
			if (_readerSettings == null)
				lock (_settingsCreateFlag)
					if (_readerSettings == null)
					{
						_readerSettings = new XmlReaderSettings();
						_readerSettings.ValidationType = ValidationType.Schema;
						_readerSettings.Schemas.Add(
							ConfigXmlConstants.XmlNamespace,
							XmlReader.Create(typeof (ConfigXmlConstants)
								.Assembly.GetManifestResourceStream(
									ConfigXmlConstants.XmlSchemaResource)));
					}
			return XmlReader.Create(innerReader, _readerSettings);
		}

		public XElement[] GetSectionContent(XName ident)
		{
			if (_loadedSections == null)
				lock (_configLoadLockFlag)
					if (_loadedSections == null)
					{
						_loadedSections = new Dictionary<XName, List<XElement>>();
						LoadSections(_dataProvider, _rootVars);
					}
			List<XElement> contents;
			if (_loadedSections.TryGetValue(ident, out contents))
				return contents.ToArray();
			else
				return null;
		}

		private void LoadSections(IConfigDataProvider provider, VarsContainer parentVars)
		{
			if (_providers.Contains(provider))
				throw new ApplicationException("Circular reference was found for '" + provider + "'");

			XDocument xDoc;
			using (XmlReader reader = provider.ReadData())
				xDoc = XDocument.Load(reader);

			var includes = new List<string>();
			var varsDic = new Dictionary<string, string>();
			var sections = new List<XElement>();
			foreach (var elem in xDoc.Root.Elements())
			{
				if (elem.Name.NamespaceName == ConfigXmlConstants.XmlNamespace)
					if (elem.Name.LocalName == ConfigXmlConstants.IncludeTagName)
					{
						var trimmed = elem.Value.Trim();
						if (trimmed == "")
							throw new ConfigurationException(ConfigurationResources.IncludeEmptyMessage);
						includes.Add(trimmed);
						continue;
					}
					else if (elem.Name.LocalName == ConfigXmlConstants.VariableTagName)
					{
						var name = elem.Attribute(ConfigXmlConstants.VariableNameAttribute).Value.Trim();
						if (name == "")
							throw new ConfigurationException(ConfigurationResources.VarNameEmptyMessage);
						if (varsDic.ContainsKey(name) || parentVars.IsVarDefined(name))
							throw new ConfigurationException(
								string.Format(ConfigurationResources.VarAlreadyDefined, name));
						varsDic.Add(name, elem.Value.Trim());
						continue;
					}
				sections.Add(elem);
			}

			var vars = new VarsContainer(varsDic, parentVars);

			foreach (var elem in sections)
			{
				SubstVars(vars, elem);
				if (_loadedSections.ContainsKey(elem.Name))
					_loadedSections[elem.Name].Add(elem);
				else
					_loadedSections.Add(elem.Name, new List<XElement>(new[] { elem }));
			}

			foreach (var include in includes)
				LoadSections(provider.ResolveInclude(include), vars);

			_providers.Add(provider);
			provider.ConfigChanged += ProviderConfigChanged;
		}

		private void SubstVars(VarsContainer vars, XElement elem)
		{
			var texts =
				from node in elem.DescendantNodesAndSelf()
				where node.NodeType == XmlNodeType.Text
				select (XText)node;
			foreach (var text in texts)
				text.Value = SubstVars(vars, text.Value);
			foreach (var attr in elem.DescendantsAndSelf().Attributes())
				attr.Value = SubstVars(vars, attr.Value);
		}

		private string SubstVars(VarsContainer vars, string str)
		{
			return _varsRegex.Replace(str,
				match => vars.GetVar(match.Groups[1].Value));
		}

		private void ProviderConfigChanged(IConfigDataProvider sender)
		{
			_loadedSections = null;
			ClearProviders();
			OnConfigChanged();
		}

		private void ClearProviders()
		{
			foreach (var provider in _providers)
				provider.ConfigChanged -= ProviderConfigChanged;
			_providers.Clear();
		}

		public event EventHandler<ConfigSerializer> ConfigChanged;

		protected void OnConfigChanged()
		{
			if (ConfigChanged != null)
				ConfigChanged(this);
		}

		#region IDisposable Members
		public void Dispose()
		{
			foreach (var provider in _providers)
			{
				var disp = provider as IDisposable;
				if (disp != null)
					disp.Dispose();
			}
			ClearProviders();
		}
		#endregion
	}
}
